###############################################################################
#
# file: io.py
# author: Victor Bos
# 
# Copyright 2007--2011 Victor Bos
#
# This file is part of GREATOR.
#
#    GREATOR is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    GREATOR is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with GREATOR.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

# standard library imports
import operator
import os.path
import random
import re
import StringIO
import sys
import types
import unittest

# Tkinter imports
import tkFileDialog

###############################################################################
class Scrambler:
    """
    A Scrambler encodes/decodes byte strings using a sequence of random bytes.
    Each byte is encoded using the equality
    
      x = c XOR i
    
    where c is the input byte, x is the encoded byte, and i is 'the next 
    random number'.
    
    An encoded byte is decoded by applying the XOR operator again, with the
    same random number i:
    
      c = x XOR i
    
    The next random number is given by randint(0,255) using a standard random
    number generator from the Python stdlib.
    
    Before decoding a string, a call to Scrambler.reset() is required in 
    order to reset the random number generator.
    
    A Scrambler's random number generator shall not be used by any other
    object. Therefore, each Scrambler creates its own random number generator. 
    """
    
    def __init__(self, key=101):
        self.rg = random.Random()
        self.key = key
        self.reset()
        
    def reset(self):
        """
        Sets the seed of the Scrambler to its initial value (which 
        is the Scrambler's key).
        """
        self.rg.seed(self.key)
        
    def encode(self, bytestring):
        if self.key == "plain":
            return bytestring
        xor = operator.xor
        randint = self.rg.randint
        return "".join([chr(xor(ord(c), randint(0,255))) for c in bytestring])

class TestScrambler(unittest.TestCase):
    
    def setUp(self):
        self.ss = [ Scrambler(i) for i in xrange(1000)]
        self.plain = Scrambler("plain")
        
    def testScrambler(self):
        buffer0 = "".join([chr(random.randint(0,255)) for i in xrange(100)])
        prev_buffer1 = None
        for s in self.ss:
            buffer1 = s.encode(buffer0)
            self.assertNotEqual(prev_buffer1, buffer1)
            prev_buffer1 = buffer1
            s.reset()
            buffer2 = s.encode(buffer1)
            self.assertEqual(buffer0, buffer2)
            self.assertNotEqual(buffer0, buffer1)

    def testPlainScrambler(self):
        buffer0 = "Dit is een nederlandse zin."
        buffer1 = self.plain.encode(buffer0)
        self.plain.reset()
        buffer2 = self.plain.encode(buffer1)
        self.assertEqual(buffer0, buffer1)
        self.assertEqual(buffer0, buffer2)
    
    def testPlatformIndep(self):
        # This function checks if the random number generator used is the same
        # as on a reference platform.
        key = 1001
        s = Scrambler(key)
        orig_txt = "This is the original text."
        orig_err_txt = "This is not the original text."
        enc_txt = '9f67abaf378eba482bce03b2f47ed5d285d9c6d010f4809570ce'.decode("hex_codec")
        enc_err_txt = '9e67abaf378eba482bce03b2f47ed5d285d9c6d010f4809570cf'.decode("hex_codec")
        self.assertEqual(s.encode(enc_txt), orig_txt)
        self.assertNotEqual(s.encode(enc_txt), orig_err_txt)
        self.assertNotEqual(s.encode(enc_err_txt), orig_txt)

###############################################################################
def module_path():
    """Returns the directory from where the main program is started."""
    return sys.path[0]

class TestModulePath(unittest.TestCase):
    
    def test_module_path(self):
        self.assertEqual(module_path(), sys.path[0])

###############################################################################
def open_dialog(
    init_filename, default_filename, ftype_descr, tkdialogfunc, title):
    if init_filename is None:
        init_filename = default_filename
    init_dirname = os.path.dirname(init_filename)
    s = tkdialogfunc(
        initialfile=init_filename,
        initialdir=init_dirname,
        filetypes=[(ftype_descr,
                    "*"+os.path.splitext(default_filename)[1]),
                   ("all files", "*")],
        title=title)
    if type(s) == types.TupleType:
        s = ""
    # Normalize the path, but replace normpath("") by ""
    s = os.path.normpath(s)
    if s == os.path.normpath(""):
        s = ""
    return s

def open_save_dialog(
    init_filename, default_filename, ftype_descr,  title="Save file"):
    return open_dialog(
        init_filename,
        default_filename,
        ftype_descr,
        tkFileDialog.asksaveasfilename,
        title)

def open_load_dialog(
    init_filename, default_filename, ftype_descr, title="Load file"):
    return open_dialog(
        init_filename,
        default_filename,
        ftype_descr,
        tkFileDialog.askopenfilename,
        title)

def write_attrs(attrs, strbuffer, spc=""):
    """
    Writes the key-value pairs of dictionary attrs to strbuffer.

    strbuffer has to have a write() method like File and StringIO
    objects have.

    Each key is written to a new line. The value follows on the same line
    as the key and is separated from the key by a colonand a space.

    Each value, except the last, is followed by a new-line \n symbol.
    
    Each key is prefixed by spc; spc can be used as indentation white space.
    """
    sep = ""
    ks = attrs.keys()
    ks.sort()
    for k in ks:
        strbuffer.write("%s%s%s: %s" % (sep, spc, k, attrs[k]))
        sep = "\n"

def attrs_to_str(attrs, spc=""):
    sbuf = StringIO.StringIO()
    write_attrs(attrs, sbuf, spc=spc)
    return sbuf.getvalue()
    
######################################################################
     
def read_attrs(strbuffer, defkey=None):
    """
    Reads a list of KEY:VALUE pairs from a string. All KEYs
    and VALUEs are strings. The KEYs are recognized by the regular expression
    
      (^|\n)\s*\w([^:\n]*\w)?\s*:

    Keys cannot extend over several lines.
    
    Keys start and end with a character matching the regular expression \w.
    
    Only the characters that match \w([^:\n]*\w)? are part of the key.

    Values can extend over several lines and they can contain : characters.
    However, multiline values with : characters might be understood as separate
    keys.
      
    strbuffer: the input string

    defkey: if not None, this is the default key.

    Returns a dictionary with KEY,VALUE pairs found in buffer.
    
    """
    attrs = {}
    endidx = 0
    key_re = re.compile(r"(^|\n)\s*(?P<attrkey>\w([^:\n]*\w)?)\s*:")
    key_mo = key_re.search(strbuffer, endidx)
    if key_mo:
        key = key_mo.group('attrkey')
        endidx = key_mo.end()
        key_mo = key_re.search(strbuffer, endidx)
        while key_mo is not None:
            i1 = key_mo.start()
            val = (strbuffer[endidx:i1]).strip()
            attrs[key] = val
            key = key_mo.group('attrkey')
            endidx = key_mo.end()
            key_mo = key_re.search(strbuffer, endidx)
        val = (strbuffer[endidx:]).strip()
        endidx = len(strbuffer)
        attrs[key] = val
    rest = (strbuffer[endidx:]).strip()
    if len(rest):
        if defkey is not None:
            attrs[defkey] = rest
    return attrs

        
###############################################################################
if __name__ == '__main__':
    unittest.main()
